/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 * 	The greatest achievement is selflessness. The greatest worth is self-mastery.
 * 	The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 * 	The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 * 	The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 * 	The greatest goodness is a peaceful mind. The greatest patience is humility.
 * 	The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 * 	The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 */
package org.javaongems.client.forms;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

import org.javaongems.client.Controller;
import org.javaongems.client.Gem;
import org.javaongems.client.View;
import org.javaongems.client.rt.text.dom.DomExtUtils;
import org.javaongems.client.stdctrls.BorderLayoutPanel;
import org.javaongems.client.stdctrls.HtmlSpan;
import org.javaongems.client.stdctrls.Image;
import org.javaongems.client.stdctrls.ProgressBar;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * <p>Provides a form/window for rendering widgets on. Each form then participates
 * in the Gem's window event management. Windowing applications have been used
 * extensively in desktop applications and typically work well with event driven 
 * systems. The Gems framework attempts to fuse a traditional web page's content as 
 * the desktop itself (ie. backdrop), with windows overlayed on top and rendered as
 * required.</p>
 * <p>Responsibilities:
 * <ul>
 * 	<li> add a form class listener to subscribe to changes in active form instances 
 * 		 (useful for window managers)
 * 	<li> support open, close, place, refresh, bring to front, show, hide signals
 * 	<li> support window resizing, moving, minimising, maximising
 * 	<li> support modal and non-modal forms
 *  <li> display title and status messages
 * </ul>
 * </p>
 * <p>Collaborators:
 * <ul>
 * 	<li> FormActionListener (used to manage all events that occur on the form)
 *  <li> FormClassListener (used to register all form construction/destruction)
 * </ul>
 * </p>
 * <p>Influences:
 * <ul>
 * 	<li> Borland Delphi's form component in the VCL
 * </ul>
 * </p>
 * <p>Configuration: <br/>
 * CSS Style Rules<br/>
 * <ul class='css'>
 * <li>.gems-Form { the outside of the dialog }</li>
 * <li>.gems-Form .Caption { the caption }</li>
 * <li>.gems-Form .Status { the status message }</li>
 * </ul>
 * </p>
 * 
 * @author Ashin Wimalajeewa (ash)
 */
public class Form extends Composite implements View {
	public interface FormClassListener {
		public void onOpenedFormListChange(Stack openedForms);
	}
	
	final static public String FORM_CONTROLLER_MODEL = "form.controller.model";
	final static public String FORM_CONTROLLER = "form.controller";
	
	final static public String FORM_RESIZE_ICON = "gems-Form-resize.gif";
	final static public String FORM_MIN_ICON = "gems-Form-min.gif";
	final static public String FORM_MAX_ICON = "gems-Form-max.gif";
	final static public String FORM_CLOSE_ICON = "gems-Form-close.gif";

	final static public String FORM_LOADING_ICON = "gems-loading.gif";

	static public Stack 	OPENED_FORM_LIST = new Stack();
	static public ArrayList	FORM_CLASS_LISTENERS = new ArrayList();
	
	protected HashMap cargo = new HashMap();
	
	protected Container container = new Container();
	protected BorderLayoutPanel titlePanel = new BorderLayoutPanel();
	protected DockPanel titleBar = new DockPanel();
	protected DockPanel statusBar = new DockPanel();
	protected String imagesCtx = Gem.stone.getApplImageCtx();
	protected HTML status = new HTML("");
	protected DockPanel formActionBar = new DockPanel();
	protected DockPanel statusInfo = new DockPanel();
	protected FlowPanel grabHandleContainer = new FlowPanel();
	protected ProgressBar progressBar = new ProgressBar();
	protected FormActionListener actionHandler;
	protected boolean opened;
	protected boolean modal;
	protected HTML closeButton;
	protected HTML maxButton;
	protected HTML minButton;
	protected HTML caption = new HTML();
	protected HTML resizeHandle;
	protected HtmlSpan titleIconCnt = new HtmlSpan();
	protected com.google.gwt.user.client.ui.Image loadingIcon;
	protected com.google.gwt.user.client.ui.Image formIcon;
	protected int nestedloadsCount = 0;
	
	/**
	 * Creates a new form object
	 * @param ctrlr - the controller to assign to this view 
	 */
	public Form(Controller ctrlr) {
		this();
		setController(ctrlr);
	}
	
	/**
	 * Creates a new form object without a controller
	 * @see #setController(Controller)
	 */
	public Form() {
		setWidget(container);
		loadFormProperties();
	}

	/**
	 * This method is intented to be populated by an IDE in the Delphi form
	 * module resource fashion. IDE's should use either serialisation or a 
	 * bean setters technique to load the resources for the form that were
	 * specified at design time in the visual editor. 
	 */
	private void loadFormProperties() {
		initialiseLayout();
		initialiseWidgets();
		setVisible(false);
	}

	/**
	 * Sets the controller for use with this view
	 * @param ctrlr - the controller to use for managing behavior
	 */
	public void setController(Controller ctrlr) {
		cargo.put(FORM_CONTROLLER, ctrlr);
	}
	
	/**
	 * @return the controller associated to this view
	 */
	public Controller getController() {
		return (Controller) cargo.get(FORM_CONTROLLER);
	}
	
	/**
	 * Sets the controller's model for this view. Views should wrap this
	 * model to create view specific representations if required
	 * @param model
	 */
	public void setModel(Object model) {
		cargo.put(FORM_CONTROLLER_MODEL, model);
	}
	
	/**
	 * @return the controller model associated to this view
	 */
	public Object getModel() {
		return cargo.get(FORM_CONTROLLER_MODEL);
	}
	
	/**
	 * @return the user-defined cargo that this view is carrying. Users
	 * can add cargo to instances to help keep objects together. 
	 */
	public Map getCargo() {
		return cargo;
	}

	/**
	 * allocate resources to those facilities that will be used by this
	 * form resource 
	 * @see #destroy()
	 */
	protected void construct() {
	}
	
	/**
	 * deallocate resources to those facilities that were previously
	 * allocated
	 * @see #construct()
	 */
	protected void destroy() {
		cargo.clear();
	}

	/**
	 * adds the main content widget to the form and centres it to occupy
	 * the client area
	 * @param widget - the widget indented to occupy the full client area
	 */
	public void add(Widget widget) {
		container.add(widget, DockPanel.CENTER);
	}

	/**
	 * @return the composite container that can be used for maniupulating
	 * the layout and/or injecting other widgets
	 */
	protected DockPanel getContainer() {
		return container;
	}
	
	/**
	 * @return the form caption used in the title bar
	 */
	public String getCaption() {
		return caption.getText();
	}

	/**
	 * Sets the form caption to be used in the title bar. Users of
	 * windows are encouraged to make this unique b/c a window manager
	 * may list all windows open on the desktop.
	 * @param caption - name of the window
	 */
	public void setCaption(String caption) {
		this.caption.setText(caption);
	}
	
	/**
	 * @return true if this form disables input to other widgets outside of the 
	 * form
	 */
	public boolean isModal() {
		return modal;
	}
	
	/**
	 * Sets the modal status of the form. A "true" value will cause all input 
	 * events outside the form to ignored until this dialog is closed. 
	 * Note, this value must be set prior to prior to the form being opened, 
	 * otherwise it will be ignored.
	 * @param modality
	 */
	public void setModal(boolean modality) {
		if (opened)
			return;
		modal  = modality;
	}

	/**
	 * @return the status text currently being displayed in the status
	 * pane
	 */
	public String getStatus() {
		return status.getText();
	}
	
	/**
	 * Sets the progress bars indicator
	 * @param perc
	 */
	public void setProgress(int perc) {
		progressBar.setPosition(perc);
	}

	/**
	 * Sets status text to be displayed in the state pane of the form 
	 * @param status
	 */
	public void setStatus(String status) {
		this.status.setText(status);
	}

	/**
	 * Sets the icon that will be displayed in the title bar
	 * @param icon
	 */
	public void setFormIcon(com.google.gwt.user.client.ui.Image icon) {
		if (formIcon == icon)
			return;
		Element elem = titleIconCnt.getElement();
		if (formIcon != null)
			DOM.removeChild(elem, formIcon.getElement());
		if (icon == null)
			return;
		formIcon = icon;
		DOM.setIntAttribute(formIcon.getElement(), "width", 16);
		DOM.setIntAttribute(formIcon.getElement(), "height", 16);
		DOM.appendChild(elem, formIcon.getElement());
	}
	
	/**
	 * @return the current icon displayed in the title bar 
	 */
	public com.google.gwt.user.client.ui.Image getFormIcon() {
		return formIcon;
	}
	
	/**
	 * changes the title bar's icon into a loading icon and displays the msg 
	 * text in the status bar.  
	 * @param msg to display in the status bar
	 */
	public void reportLoadingStatus(String msg) {
		if (nestedloadsCount == 0) {
			Element elem = titleIconCnt.getElement();
			if (formIcon != null) 
				removeImgCntFirstChild(elem);
			DOM.appendChild(elem, loadingIcon.getElement());
		}
		setStatus(msg);
		nestedloadsCount++;
	}

	/**
	 * Toggles the title bar's icon back to the original icon from the loading 
	 * icon and clears the status message.
	 */
	public void closeLoadingStatusReport() {
		nestedloadsCount--;
		if (nestedloadsCount == 0) {
			Element elem = titleIconCnt.getElement();
			removeImgCntFirstChild(elem);
			if (formIcon != null) 
				DOM.appendChild(elem, formIcon.getElement());
			setStatus("");
		}
	}
	
	/**
	 * Removes the first child element from the image container.
	 * @param elem - the image container 
	 */
	private void removeImgCntFirstChild(Element elem) {
		Element firstChild = DOM.getChild(elem, 0);
		if (firstChild != null)
			DOM.removeChild(elem, firstChild);
	}
	
	/**
	 * Position the form to absolute client area positions and then sizes the 
	 * form.
	 * @param leftTopWidthHeight
	 */
	public void place(int[] leftTopWidthHeight) {
		setPosition(leftTopWidthHeight[0], leftTopWidthHeight[1]);
		setPixelSize(leftTopWidthHeight[2], leftTopWidthHeight[3]);
	}

	/**
	 * @return the action bar pane associated with the window. The action
	 * bar pane is used to display border icons of min, max & close.
	 */
	protected DockPanel getFormActionBar() {
		return formActionBar;
	}
	
	/**
	 * Renders the form on the browser page
	 * @see #hide()
	 * @see FormActionListener#onFormShow()
	 */
	public void show() {
		if (isVisible())
			return;
		setVisible(true);
	}

	/**
	 * Hides the form from the browser page
	 * @see #show()
	 * @see FormActionListener#onFormHide()
	 */
	public void hide() {
		if (!isVisible())
			return;
		setVisible(false);
	}
	
	/**
	 * Brings the form to the top of the window stack
	 */
	public void bringToFront() {
		if (OPENED_FORM_LIST.peek() != this) {
			OPENED_FORM_LIST.remove(this);
			OPENED_FORM_LIST.push(this);
			reorganiseFormsZindex();
			fireFormClassChangeNotifcation();
		}
	}
	
	/**
	 * @return true if the form has been prepared and ready for rendering
	 */
	public boolean isOpen() {
		return opened;
	}
	
	/**
	 * Prepares the form for rendering by attaching it to the root panel. 
	 * Preparation may include allocating resources and other user-defined
	 * functionality invoked in the onFormOpened() handler
	 * 
	 * @see #construct()
	 * @see FormActionListener#onFormOpened()
	 */
	public void open() {
		if (!opened) {
			opened = true;
			construct();
			RootPanel.get().add(this);
			if (actionHandler != null)
				actionHandler.onFormOpened();
		}
	}

	/**
	 * Prepares the form for closure by detaching it from the root panel.
	 * Preparation may include de-allocating resources and other user-defined
	 * functionality invoked in the onFormClosed handler
	 * 
	 * @see #open()
	 * @see FormActionListener#onFormClosed()
	 */
	public void close() {
		if (opened) {
			opened = false;
			hide();
			if (OPENED_FORM_LIST.contains(this)) {
				OPENED_FORM_LIST.remove(this);
				reorganiseFormsZindex();
				fireFormClassChangeNotifcation();
			}
			if (actionHandler != null)
				actionHandler.onFormClosed();
			RootPanel.get().remove(this);
			destroy();
		}
	}
	
	/**
	 * Core operation used in order to satisfy the show/hide functionality
	 * @see #open()
	 * @see #hide()
	 */
	public void setVisible(boolean visible) {
		if (isVisible() != visible) {
			if (visible)
				open();
			if (actionHandler != null) {
				if (visible) 
					actionHandler.onFormShow();
				else
					actionHandler.onFormHide();
			}
			super.setVisible(visible);
			if (visible) {
				if (OPENED_FORM_LIST.contains(this)) {
					OPENED_FORM_LIST.remove(this);
					OPENED_FORM_LIST.push(this);
				} else 
					OPENED_FORM_LIST.push(this);
				reorganiseFormsZindex();
				fireFormClassChangeNotifcation();
			}
		}
	}
	
	/**
	 * Refresh is intended to invalidate the rendering area such that
	 * it will be redrawn with latest data from the model
	 */
	public void refresh() {
		if (actionHandler != null)
			actionHandler.onFormRefresh();
	}
	
	/**
	 * Changes the position of the form to the specified location
	 * @param left - absolute left position
	 * @param top  - absolute top position
	 */
	public void setPosition(int left, int top) {
		RootPanel.get().setWidgetPosition(this, left, top);
	}

	/**
	 * @return true if the close button is active
	 */
	public boolean isCloseFormButtonDisplayed() {
		return closeButton.isVisible();
	}

	/**
	 * Causes the close button to either be, or not be active in the form
	 * @param display - true will make the button active
	 */
	public void setCloseFormButtonDisplayed(boolean display) {
		closeButton.setVisible(display);
	}

	/**
	 * @return true if the min button is active
	 */
	public boolean isMinFormButtonDisplayed() {
		return minButton.isVisible();
	}

	/**
	 * Causes the min button to either be, or not be active in the form
	 * @param display - true will make the button active
	 */
	public void setMinFormButtonDisplayed(boolean display) {
		minButton.setVisible(display);
	}

	/**
	 * @return true if the max button is active
	 */
	public boolean isMaxFormButtonDisplayed() {
		return maxButton.isVisible();
	}

	/**
	 * Causes the max button to either be, or not be active in the form
	 * @param display - true will make the button active
	 */
	public void setMaxFormButtonDisplayed(boolean display) {
		maxButton.setVisible(display);
	}

	/**
	 * Causes the entire action bar button group to be active/inactive 
	 * @param display - true will make each button active
	 */
	public void setFormActionButtonsDisplayed(boolean display) {
		setCloseFormButtonDisplayed(display);
		setMinFormButtonDisplayed(display);
		setMaxFormButtonDisplayed(display);
	}

	/**
	 * @return true if the resize handle is to be displayed
	 */
	public boolean isResizeFormButtonDisplayed() {
		return resizeHandle.isVisible();
	}

	/**
	 * Causes the resize handle to either be, or not be active in the form
	 * @param display - true will make the handle active
	 */
	public void setResizeFormButtonDisplayed(boolean display) {
		resizeHandle.setVisible(display);
	}

	/**
	 * @return true of the status pane is active
	 */
	public boolean isStatusBarDisplayed() {
		return statusBar.isVisible();
	}

	/**
	 * Sets the status bar (which includes the resize handle) to be (in)/active
	 * @param display - true will make the status pane active
	 */
	public void setStatusBarDisplayed(boolean display) {
		statusBar.setVisible(display);
	}

	/**
	 * @return the status pane to enable its layout to be manipulated or 
	 * other widgets to be injected
	 */
	protected DockPanel getStatusBar() {
		return statusBar;
	}

	/**
	 * Sets the action handler that will be signaled upon specified events
	 * occuring to the form.
	 * 
	 * @see FormActionListener
	 * @param listener - handler that will be given the opportunity to run
	 * upon enlisted events
	 */
	public void setFormActionListener(FormActionListener listener) {
		actionHandler = listener;
	}
	
	/**
	 * @return the absolute top position of the status bar. This is convenient
	 * when widget panels are not resizing themselves.
	 */
	protected int getStatusBarTop() {
		return statusBar.getAbsoluteTop();
	}
	
	/**
	 * @return the absolute width position of the status bar. This is convenient
	 * when widget panels are not resizing themselves.
	 */
	protected int getStatusBarWidth() {
		return statusBar.getOffsetWidth();
	}
	
	/**
	 * Adds a form class listener to the classifier as opposed to each
	 * individual instance. This facility is typically only useful to
	 * window managers.
	 * @param lsnr - the listener to add
	 */
	static public void addFormClassListener(FormClassListener lsnr) {
		FORM_CLASS_LISTENERS.add(lsnr);
	}
	
	
	/**
	 * reorders each forms zindex position
	 */
	static private void reorganiseFormsZindex() {
		int cntr = 1;
		for (Iterator iter = OPENED_FORM_LIST.iterator(); iter.hasNext();) {
			Form frm = (Form) iter.next();
			Element frmElem = frm.getElement();
			DomExtUtils.setElementZindex(frmElem, cntr++);
		}
	}
	
	/**
	 * Send notifications to those class listeners
	 */
	static private void fireFormClassChangeNotifcation() {
		for (Iterator iter = FORM_CLASS_LISTENERS.iterator(); iter.hasNext();) {
			FormClassListener element = (FormClassListener) iter.next();
			element.onOpenedFormListChange(OPENED_FORM_LIST);
		}
	}
	
	/**
	 * Organises the style and arrangement of system widgets on the form 
	 */
	private void initialiseLayout() {
		setStyleName("gems-Form");
		container.add(titlePanel, DockPanel.NORTH);
		container.add(statusBar, DockPanel.SOUTH);

		titlePanel.setWidget(titleBar, BorderLayoutPanel.CENTER);
		
		titleBar.setStyleName("Caption");
		closeButton = new HTML("<img src='" + imagesCtx + FORM_CLOSE_ICON + "'>");
		maxButton = new HTML("<img src='" + imagesCtx + FORM_MAX_ICON + "'>");
		minButton = new HTML("<img src='" + imagesCtx + FORM_MIN_ICON + "'>");
		resizeHandle = new HTML("<img src='" + imagesCtx + FORM_RESIZE_ICON + "'>");
		loadingIcon = new Image(imagesCtx + FORM_LOADING_ICON);
		loadingIcon.setPixelSize(16, 16);
		
		titleBar.add(titleIconCnt, DockPanel.WEST);
		titleBar.add(formActionBar, DockPanel.EAST);
		titleBar.add(caption, DockPanel.CENTER);
		formActionBar.add(closeButton, DockPanel.EAST);
		formActionBar.add(maxButton, DockPanel.EAST);
		formActionBar.add(minButton, DockPanel.EAST);
		formActionBar.setWidth("66px");
		titleBar.setCellWidth(caption, "100%");
		titleBar.setCellHorizontalAlignment(formActionBar, DockPanel.ALIGN_RIGHT);
	}

	/**
	 * Initialises the system widgets on the form
	 */
	private void initialiseWidgets() {
		DOM.setStyleAttribute(titleIconCnt.getElement(), "padding", "5");
		status.setStyleName("Status");
		statusBar.setStyleName("Status");
		statusBar.add(grabHandleContainer, DockPanel.EAST);
		statusBar.add(progressBar, DockPanel.EAST);
		statusBar.add(statusInfo, DockPanel.CENTER);
		grabHandleContainer.add(resizeHandle);
		statusBar.setHeight("15px");
		statusInfo.add(status, DockPanel.WEST);
		statusBar.setCellWidth(statusInfo, "100%");
		grabHandleContainer.setWidth("15px");

		statusBar.setCellHorizontalAlignment(grabHandleContainer,
				DockPanel.ALIGN_RIGHT);
		statusBar.setCellVerticalAlignment(grabHandleContainer,
				DockPanel.ALIGN_BOTTOM);
		container.setCellVerticalAlignment(statusBar, DockPanel.ALIGN_BOTTOM);
	}
	
	protected class Container extends DockPanel {
		protected Element elm;
		public Container() {
			sinkEvents(Event.ONCLICK);
			elm = getElement();
		}
		
		public void onBrowserEvent(Event event) {
			int type = DOM.eventGetType(event);
			switch (type) {
				case Event.ONCLICK: {
					Form.this.bringToFront();
				}
			}
		}
	}
}